package org.sample.springbootcommandlinerunner.web;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ObjectUtils;

import java.io.FileWriter;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class StopWatch {

	private static final Logger LOG = LoggerFactory.getLogger(StopWatch.class);

	private final int taskCount;

	private final List<List<Instant>> stepTimes;

	private final List<List<String>> marks;

	private final String[] errors;

	private final String[] stepDescriptions;

	private final String[] sessionIds;

	private final String[] conversationIds;

	public StopWatch(int taskCount, String[] stepDescriptions) {

		this.taskCount = taskCount;

		stepTimes = new ArrayList<>(taskCount);

		for (int i = 0; i < taskCount; i++) {
			stepTimes.add(new ArrayList<>(stepDescriptions.length));
		}

		marks = new ArrayList<>(taskCount);

		for (int i = 0; i < taskCount; i++) {
			marks.add(new ArrayList<>(stepDescriptions.length));
		}

		errors = new String[taskCount];

		sessionIds = new String[taskCount];
		conversationIds = new String[taskCount];

		this.stepDescriptions = stepDescriptions;
	}

	public void recordStepTime(int taskNum) {
		recordStepTime(taskNum, null);
	}

	public void recordStepTime(int taskNum, String mark) {

		stepTimes.get(taskNum).add(Instant.now());

		if (mark == null) {
			mark = "";
		}
		marks.get(taskNum).add(mark);

		if (LOG.isDebugEnabled()) {
			LOG.debug("[{}] step done: {} {}", taskNum, stepDescriptions[stepTimes.get(taskNum).size() - 1], mark);
		}
	}

	public void recordStepError(int taskNum, String error) {

		errors[taskNum] = error;

		if (LOG.isDebugEnabled()) {
			LOG.debug("[{}] error: {}", taskNum, error);
		}
	}

	public int taskCount() {
		return this.taskCount;
	}

	public String getTaskSessionId(int taskNum) {
		return sessionIds[taskNum];
	}

	public void setTaskSessionId(int taskNum, String sessionId) {
		sessionIds[taskNum] = sessionId;
	}

	public String getTaskConversationId(int taskNum) {
		return conversationIds[taskNum];
	}

	public void setTaskConversationId(int taskNum, String conversationId) {
		conversationIds[taskNum] = conversationId;
	}

	public void report(Consumer<String> reportee) {

		for (int i = 0; i < stepTimes.size(); i++) {

			var taskStepTimes = stepTimes.get(i);
			var taskMarks = marks.get(i);

			for (int j = 0; j < taskStepTimes.size(); j++) {

				String report;
				if (j == 0) {
					report = String.format("Task (%d): %s %s %s", i,
							taskStepTimes.get(j), taskMarks.get(j), stepDescriptions[j]);
				} else {
					report = String.format("Task (%d): %s %s %s", i,
							Duration.between(taskStepTimes.get(j - 1), taskStepTimes.get(j)).toMillis() + " milliseconds",
							taskMarks.get(j), stepDescriptions[j]);
				}

				reportee.accept(report);
			}

			var error = errors[i];
			if (error != null) {
				reportee.accept(error);
			}
		}
	}

	public void saveAsCsv(String path) throws IOException {

		List<String> headers = new ArrayList<>();
		headers.add("number");
		headers.addAll(Arrays.asList(stepDescriptions));
		headers.add("mark");
		headers.add("error");
		try (var out = new FileWriter(path);
				var printer = new CSVPrinter(out,
				CSVFormat.DEFAULT.withHeader(headers.toArray(new String[0])))) {

			var stepCount = stepDescriptions.length;
			for (int i = 0; i < stepTimes.size(); i++) {

				var taskStepTimes = stepTimes.get(i);
				var taskMarks = marks.get(i);

				List<String> values = new ArrayList<>();

				values.add(Integer.toString(i));

				for (int j = 0; j < stepCount; j++) {

					if (j >= taskStepTimes.size()) {

						values.add("");

					} else if (j == 0) {

						values.add(taskStepTimes.get(j).atOffset(ZoneOffset.ofHours(8)).toString());

					} else {

						var duration = Duration.between(taskStepTimes.get(j - 1), taskStepTimes.get(j)).toMillis();
						values.add(Long.toString(duration));
					}
				}

				var markValue = taskMarks.stream()
						.filter(mark -> !ObjectUtils.isEmpty(mark))
						.reduce((a, b) -> a + " " + b)
						.orElse("");
				values.add(markValue);

				var error = errors[i];
				if (error != null) {
					values.add(error);
				} else {
					values.add("");
				}

				printer.printRecord(values);
			}
		}
	}

}
